﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using NesEmulatorX.Exceptions;

namespace NesEmulatorX
{
	public class CPUMemory
	{
		#region Constants

		public const int BASE_ADDRESS = 0x0000;
		public const int MAX_ADDRESS = 0xFFFF;

		public const int BASE_RAM_ADDRESS = 0x0000;
		public const int MAX_RAM_ADDRESS = 0x1FFF;

		public const int RAM_BANK_SIZE = 0x800;
		public const int RAM_BANK_COUNT = 4;

		public const int BASE_REGISTERS_ADDRESS = 0x2000;
		public const int MAX_REGISTERS_ADDRESS = 0x401F;

		public const int LAST_REGISTERS_BANK_ADDRESS = 0x4000;
		public const int LAST_REGISTERS_BANK_SIZE = 0x20;

		public const int REGISTERS_BANK_SIZE = 0x8;
		public const int REGISTERS_BANK_COUNT = 1023;

		public const int BASE_EXPANSION_ROM_ADDRESS = 0x4020;
		public const int MAX_EXPANSION_ROM_ADDRESS = 0x5FFF;

		public const int BASE_SRAM_ADDRESS = 0x6000;
		public const int MAX_SRAM_ADDRESS = 0x7FFF;

		public const int BASE_PRG_ROM1_ADDRESS = 0x8000;
		public const int MAX_PRG_ROM1_ADDRESS = 0xBFFF;

		public const int BASE_PRG_ROM2_ADDRESS = 0xC000;
		public const int MAX_PRG_ROM2_ADDRESS = 0xFFFF;

		public const int NMI_INTERUPT_ADDRESS = 0xFFFA;
		public const int RESET_INTERUPT_ADDRESS = 0xFFFC;
		public const int IRQ_BRK_INTERUPT_ADDRESS = 0xFFFE;

		#endregion

		#region Properties

		internal byte[] Memory
		{
			get { return _memory; }
		}
		private byte[] _memory = new byte[CPUMemory.MAX_ADDRESS + 1];

		public bool EnableSRAMWrite
		{
			get { return _enableSRAMWrite; }
			set { _enableSRAMWrite = value; }
		}
		private bool _enableSRAMWrite = true;

		#endregion

		#region Public Methods

		public void Reset(bool isPowerOn = false)
		{
			if (isPowerOn)
			{
				this.ResetMemory(0x0);
			}
		}

		public void Write(int address, byte value)
		{
			if (address > CPUMemory.MAX_ADDRESS || address < CPUMemory.BASE_ADDRESS)
				throw new NESMemoryWriteOutOfBoundsException(String.Format("Cannot write to address {0} in RAM", address));

			if (address >= CPUMemory.BASE_RAM_ADDRESS && address <= CPUMemory.MAX_RAM_ADDRESS)
			{
				address %= CPUMemory.RAM_BANK_SIZE; // only write to the first bank as the ram memory is mirrored - speeds things up only reading/writting from one location rather than many
			}
			else if (address >= CPUMemory.BASE_REGISTERS_ADDRESS && address <= CPUMemory.MAX_REGISTERS_ADDRESS)
			{
				address %= CPUMemory.REGISTERS_BANK_SIZE;
				address += CPUMemory.BASE_REGISTERS_ADDRESS;
			}
			else if (address >= CPUMemory.BASE_EXPANSION_ROM_ADDRESS && address <= CPUMemory.MAX_EXPANSION_ROM_ADDRESS)
			{
				return;
			}
			else if (address >= CPUMemory.BASE_SRAM_ADDRESS && address <= CPUMemory.MAX_SRAM_ADDRESS)
			{
				if (!this.EnableSRAMWrite)
					return;
			}
			else if (address >= CPUMemory.BASE_PRG_ROM1_ADDRESS && address <= CPUMemory.MAX_PRG_ROM1_ADDRESS)
			{
				return;
			}
			else if (address >= CPUMemory.BASE_PRG_ROM2_ADDRESS && address <= CPUMemory.MAX_PRG_ROM2_ADDRESS)
			{
				return;
			}

			this.Memory[address] = value;
		}

		public byte Read(int address)
		{
			if(address < CPUMemory.BASE_ADDRESS || address > CPUMemory.MAX_ADDRESS)
				throw new NESMemoryReadOutOfBoundsException(String.Format("Cannot read from address '{0}'", address));

			return this.Memory[address];
		}

		#endregion

		#region Private Methods

		private void ResetMemory(int value)
		{
			for (int i = 0; i < CPUMemory.MAX_ADDRESS; i++)
			{
				this.Memory[i] = 0x0;
			}
		}

		#endregion
	}
}
